Skip to main content

Embed Server

The embed server auto-deploys on every push to main. The widget is then available to any site at https://webreader.abair.ie/script.js.

Pipeline

StageTriggerWhat happens
CIPR to mainRuns the build to verify the bundle still compiles.
CDMerge to mainBuilds a Docker image, pushes to the internal registry, deploys via SSH.
Livehttps://webreader.abair.ie

Cache lag after a deploy is minutes — the server emits must-revalidate so browsers and Cloudflare check back quickly.

If a deploy doesn't appear

  1. Check the Actions tab on GitHub for a failed workflow.
  2. SSH to the host and run docker ps — the container's CREATED timestamp tells you whether the new image actually started.
  3. Most likely cause: the commit never got pushed. CD only sees origin/main; local edits picked up by npm run dev aren't in the deployed bundle.

Rolling back

Revert the bad commit on main and push. CD rebuilds and redeploys. The widget URL stays the same — browsers pick up the rollback within minutes.


Embedding the production widget on a site

Once deployed, any site can load the widget with a single <script> tag.

One-line install

<script src="https://webreader.abair.ie/script.js" async></script>

Configuration

Set window.WebReaderConfig before the script loads. The boot script reads it once on init.

<script>
window.WebReaderConfig = {
locale: 'ga', // 'ga' (Irish) or 'en' (English)
panelPosition: { top: 80, left: 16 } // initial panel position in pixels
};
</script>
<script src="https://webreader.abair.ie/script.js" async></script>
OptionTypeDefaultDescription
locale'ga' | 'en''ga'UI language of the floating panel
panelPosition{ top, left, right, bottom } (px){ bottom: 16, right: 16 }Starting position; user can drag from here

React / Next.js (the canonical ABAIR pattern)

Load from a client component so it skips SSR. Re-mounts on locale change.

"use client";

import { useEffect } from "react";
import { useTranslation } from "react-i18next";

export default function Webreader() {
const { i18n } = useTranslation();

useEffect(() => {
const scriptId = "webreader-cdn-script";
if (document.getElementById(scriptId)) return;

const baseUrl =
process.env.NEXT_PUBLIC_WEBREADER_URL || "https://webreader.abair.ie";

(window as any).WebReaderConfig = {
locale: i18n.language === "ga-IE" ? "ga" : "en",
panelPosition: { top: 80, left: 16 },
};

const script = document.createElement("script");
script.id = scriptId;
script.src = `${baseUrl}/script.js`;
script.async = true;
document.body.appendChild(script);
}, [i18n.language]);

return null;
}

Mount <Webreader /> once in the root layout. The scriptId guard prevents double-mounting from route changes, hot reloads, or React StrictMode.

Environment switching

Use a public env var so the URL is available in the browser bundle:

# .env.local
NEXT_PUBLIC_WEBREADER_URL=http://localhost:3010

Leave it unset in production — the component falls back to https://webreader.abair.ie. Restart npm run dev after changing it. Next.js bakes NEXT_PUBLIC_* vars at server start.

Marking up the host page

Widget behaviour depends on the host page's HTML. Quick wins:

  • Wrap content in <main> or <article>. Anything outside is silently skipped.
  • Use real <h1><h6>, not styled <div>s. Headings get a longer pause, which helps listeners orient.
  • <a> nested in <p>/<li>/<h*> is handled smartly — "Link: " is injected inline without duplicating the text. <button> in the same position still reads twice — avoid it.
  • Form labels aren't read. Describe forms in a sibling <p> if listeners need them.

See Architecture for the full picture of what gets read and in what order.

Verification checklist

After wiring it up:

  1. Network: script.js returns 200 from the URL you expect.
  2. DOM: a <div class="webreader-panel"> lives at the end of <body>.
  3. Console: filter by WebReader — no errors.
  4. Play test: a homepage, an article, a form, a list view.
  5. Locale test: flip the language switcher, reopen the panel — UI strings should match.

Dynamic locale changes

The widget reads window.WebReaderConfig.locale once on init. The React example above re-runs its effect on i18n.language, which works because the new <script> tag overwrites the existing one. If you want an explicit tear-down + re-mount instead:

useEffect(() => {
document.getElementById("webreader-cdn-script")?.remove();
document.querySelector(".webreader-panel")?.remove();

(window as any).WebReaderConfig = { locale: currentLocale };
// ...append new script tag
}, [currentLocale]);

Most users pick a language and stick with it, so the cheaper "mount once" version is usually enough.

CORS

No extra setup needed. The widget calls https://synthesis.abair.ie directly, and the TTS API already allows cross-origin.

Self-hosting

If you ever need to host the widget at a different domain, build it as described in Development → Embed Server and serve script.js from any static host. Also serve the help and privacy pages so the in-widget help link still works.

Common gotchas

SymptomCause / fix
CSP blocks the scriptAdd https://webreader.abair.ie to script-src, https://synthesis.abair.ie to connect-src and media-src.
Reads header/footerMissing <main> — controller falls back to <body>.
Widget appears twiceDuplicate <script> tag (often from hot reload). The scriptId guard prevents it.
Nothing audibleCheck DevTools → Network for synthesise requests. The widget falls back to browser speech synthesis if the TTS API is unreachable.